{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "eec680f5",
   "metadata": {},
   "source": [
    "# Human-in-the-loop\n",
    "\n",
    "에이전트는 신뢰할 수 없으며 작업을 성공적으로 수행하기 위해 인간의 입력이 필요할 수 있습니다. \n",
    "\n",
    "마찬가지로, 일부 작업에 대해서는 모든 것이 의도한 대로 실행되고 있는지 확인하기 위해 실행 전에 **사람이 직접 개입하여 \"승인\"** 을 요구하고 싶을 수 있습니다.\n",
    "\n",
    "LangGraph는 여러 가지 방법으로 `human-in-the-loop` 워크플로를 지원합니다. \n",
    "\n",
    "이번 튜토리얼의 시작은 LangGraph의 `interrupt_before` 기능을 사용하여 항상 도구 노드를 중단하도록 하겠습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "de9d9d8d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b5c6228",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH17-LangGraph-Modules\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a6bc201a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated, List, Dict\n",
    "from typing_extensions import TypedDict\n",
    "\n",
    "from langchain_core.tools import tool\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "from langgraph.graph.message import add_messages\n",
    "from langgraph.prebuilt import ToolNode, tools_condition\n",
    "from langchain_teddynote.graphs import visualize_graph\n",
    "from langchain_teddynote.tools import GoogleNews\n",
    "\n",
    "\n",
    "########## 1. 상태 정의 ##########\n",
    "# 상태 정의\n",
    "class State(TypedDict):\n",
    "    # 메시지 목록 주석 추가\n",
    "    messages: Annotated[list, add_messages]\n",
    "\n",
    "\n",
    "########## 2. 도구 정의 및 바인딩 ##########\n",
    "# 도구 초기화\n",
    "# 키워드로 뉴스 검색하는 도구 생성\n",
    "news_tool = GoogleNews()\n",
    "\n",
    "\n",
    "@tool\n",
    "def search_keyword(query: str) -> List[Dict[str, str]]:\n",
    "    \"\"\"Look up news by keyword\"\"\"\n",
    "    news_tool = GoogleNews()\n",
    "    return news_tool.search_by_keyword(query, k=5)\n",
    "\n",
    "\n",
    "tools = [search_keyword]\n",
    "\n",
    "# LLM 초기화\n",
    "llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
    "\n",
    "# 도구와 LLM 결합\n",
    "llm_with_tools = llm.bind_tools(tools)\n",
    "\n",
    "\n",
    "########## 3. 노드 추가 ##########\n",
    "# 챗봇 함수 정의\n",
    "def chatbot(state: State):\n",
    "    # 메시지 호출 및 반환\n",
    "    return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
    "\n",
    "\n",
    "# 상태 그래프 생성\n",
    "graph_builder = StateGraph(State)\n",
    "\n",
    "# 챗봇 노드 추가\n",
    "graph_builder.add_node(\"chatbot\", chatbot)\n",
    "\n",
    "\n",
    "# 도구 노드 생성 및 추가\n",
    "tool_node = ToolNode(tools=tools)\n",
    "\n",
    "# 도구 노드 추가\n",
    "graph_builder.add_node(\"tools\", tool_node)\n",
    "\n",
    "# 조건부 엣지\n",
    "graph_builder.add_conditional_edges(\n",
    "    \"chatbot\",\n",
    "    tools_condition,\n",
    ")\n",
    "\n",
    "########## 4. 엣지 추가 ##########\n",
    "\n",
    "# tools > chatbot\n",
    "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
    "\n",
    "# START > chatbot\n",
    "graph_builder.add_edge(START, \"chatbot\")\n",
    "\n",
    "# chatbot > END\n",
    "graph_builder.add_edge(\"chatbot\", END)\n",
    "\n",
    "########## 5. MemorySaver 추가 ##########\n",
    "\n",
    "# 메모리 저장소 초기화\n",
    "memory = MemorySaver()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8aa1673",
   "metadata": {},
   "source": [
    "이제 그래프를 컴파일하고, `tools` 노드 전에 `interrupt_before`를 지정하십시오."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "60cfaf0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "########## 6. interrupt_before 추가 ##########\n",
    "\n",
    "# 그래프 빌더 컴파일\n",
    "graph = graph_builder.compile(checkpointer=memory)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20e87724",
   "metadata": {},
   "outputs": [],
   "source": [
    "########## 7. 그래프 시각화 ##########\n",
    "# 그래프 시각화\n",
    "visualize_graph(graph)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b26d4039",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.messages import pretty_print_messages\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "\n",
    "# 질문\n",
    "question = \"AI 관련 최신 뉴스를 알려주세요.\"\n",
    "\n",
    "# 초기 입력 State 를 정의\n",
    "input = State(messages=[(\"user\", question)])\n",
    "\n",
    "# config 설정\n",
    "config = RunnableConfig(\n",
    "    recursion_limit=10,  # 최대 10개의 노드까지 방문. 그 이상은 RecursionError 발생\n",
    "    configurable={\"thread_id\": \"1\"},  # 스레드 ID 설정\n",
    "    tags=[\"my-rag\"],  # Tag\n",
    ")\n",
    "\n",
    "for event in graph.stream(\n",
    "    input=input,\n",
    "    config=config,\n",
    "    stream_mode=\"values\",\n",
    "    interrupt_before=[\"tools\"],  # tools 실행 전 interrupt(tools 노드 실행 전 중단)\n",
    "):\n",
    "    for key, value in event.items():\n",
    "        # key 는 노드 이름\n",
    "        print(f\"\\n[{key}]\\n\")\n",
    "\n",
    "        # value 는 노드의 출력값\n",
    "        # print(value)\n",
    "        pretty_print_messages(value)\n",
    "\n",
    "        # value 에는 state 가 dict 형태로 저장(values 의 key 값)\n",
    "        if \"messages\" in value:\n",
    "            print(f\"메시지 개수: {len(value['messages'])}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "889d388e",
   "metadata": {},
   "source": [
    "그래프 상태를 확인하여 제대로 작동했는지 확인해 봅시다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ebcdde46",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 그래프 상태 스냅샷 생성\n",
    "snapshot = graph.get_state(config)\n",
    "\n",
    "# 다음 스냅샷 상태\n",
    "snapshot.next"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b80ebad2",
   "metadata": {},
   "source": [
    "(이전의 튜토리얼에서는) `__END__` 도달했기 때문에 `.next` 가 존재하지 않았습니다.\n",
    "\n",
    "하지만, 지금은 `.next` 가 `tools` 로 지정되어 있습니다.\n",
    "\n",
    "이제 그럼 도구 호출을 확인해 봅시다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1570ed38",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.messages import display_message_tree\n",
    "\n",
    "# 메시지 스냅샷에서 마지막 메시지 추출\n",
    "existing_message = snapshot.values[\"messages\"][-1]\n",
    "\n",
    "# 메시지 트리 표시\n",
    "display_message_tree(existing_message.tool_calls)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7062b94",
   "metadata": {},
   "source": [
    "다음으로는 이전에 종료된 지점 이후부터 **이어서 그래프를 진행** 해 보는 것입니다.\n",
    "\n",
    "LangGraph 는 계속 그래프를 진행하는 것 쉽게 할 수 있습니다.\n",
    "\n",
    "- 단지 입력에 `None`을 전달하면 됩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8f48c339",
   "metadata": {},
   "outputs": [],
   "source": [
    "# `None`는 현재 상태에 아무것도 추가하지 않음\n",
    "events = graph.stream(None, config, stream_mode=\"values\")\n",
    "\n",
    "# 이벤트 반복 처리\n",
    "for event in events:\n",
    "    # 메시지가 이벤트에 포함된 경우\n",
    "    if \"messages\" in event:\n",
    "        # 마지막 메시지의 예쁜 출력\n",
    "        event[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8d655b90",
   "metadata": {},
   "source": [
    "이제, `interrupt`를 사용하여 챗봇에 인간이 개입할 수 있는 실행을 추가하여 필요할 때 인간의 감독과 개입을 가능하게 했습니다. 이는 추후에 시스템으로 구현할때, 잠재적인 UI를 제공할 수 있ㅅ브니다.\n",
    "\n",
    "이미 **checkpointer**를 추가했기 때문에, 그래프는 **무기한** 일시 중지되고 언제든지 다시 시작할 수 있습니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "76a3baa8",
   "metadata": {},
   "source": [
    "아래는 `get_state_history` 메서드를 사용하여 상태 기록을 가져오는 방법입니다.\n",
    "\n",
    "상태 기록을 통해 원하는 상태를 지정하여 **해당 지점에서 다시 시작** 할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0b9d5d8d",
   "metadata": {},
   "outputs": [],
   "source": [
    "to_replay = None\n",
    "\n",
    "# 상태 기록 가져오기\n",
    "for state in graph.get_state_history(config):\n",
    "    # 메시지 수 및 다음 상태 출력\n",
    "    print(\"메시지 수: \", len(state.values[\"messages\"]), \"다음 노드: \", state.next)\n",
    "    print(\"-\" * 80)\n",
    "    # 특정 상태 선택 기준: 채팅 메시지 수\n",
    "    if len(state.values[\"messages\"]) == 3:\n",
    "        to_replay = state"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ff8faca6",
   "metadata": {},
   "source": [
    "그래프의 모든 단계에 대해 체크포인트가 저장된다는 점에 **주목** 할 필요가 있습니다.\n",
    "\n",
    "원하는 지점은 `to_replay` 변수에 저장합니다. 이를 활용하여 다시 시작할 수 있는 지점을 지정할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6da2eeda",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 다음 항목의 다음 요소 출력\n",
    "print(to_replay.next)\n",
    "\n",
    "# 다음 항목의 설정 정보 출력\n",
    "print(to_replay.config)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf90af22",
   "metadata": {},
   "source": [
    "`to_replay.config` 에 `checkpoint_id` 가 포함되어 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "74548a50",
   "metadata": {},
   "outputs": [],
   "source": [
    "to_replay.config"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "81b261e1",
   "metadata": {},
   "source": [
    "이 `checkpoint_id` 값을 제공하면 LangGraph의 체크포인터가 그 시점의 상태를 **로드** 할 수 있습니다.\n",
    "\n",
    "- 단, 이때는 입력값을 `None`으로 전달해야 합니다.\n",
    "\n",
    "아래의 예제를 통해 확인해 봅시다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18f5474a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# `to_replay.config`는 `checkpoint_id`는 체크포인터에 저장된 상태에 해당\n",
    "for event in graph.stream(None, to_replay.config, stream_mode=\"values\"):\n",
    "    # 메시지가 이벤트에 포함된 경우\n",
    "    if \"messages\" in event:\n",
    "        # 마지막 메시지 출력\n",
    "        event[\"messages\"][-1].pretty_print()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
